--Network Music Player Daemon alpha by Hexxeh

--What port do we listen for requests on? Default is fine, don't change unless you've got a good reason
PORT = 2222

--Killserver password
KILLPASSWORD = "cake"

--Debug mode? Leave this on for testing
DEBUG = true

---------------------------------------
--DON'T EDIT BELOW HERE!
---------------------------------------
require("socket")
require("copas")
require("lfs")
package.loadlib("luacom.dll","luacom_open") ()
print("-------------------------------------------")
print("----Network Music Player Daemon Started----")
print("-------------------------------------------")
print("-------------------------- Alpha Version --")
print("-------------------------------------------")
VERSION = "ALPHA"
connections = 0
songcount = 0
libraryfile = io.open("library.nmpd")
if libraryfile ~= nil then
	LIBRARYTEXT = string.gsub(string.gsub(libraryfile:read("*a"),"\n","@@"),"\t","$$")
	libraryfile:close()
else
	LIBRARYTEXT = ""
end
iTunes = luacom.CreateObject("iTunes.Application")
iTunesEvents = {}
trackevents = {}

function printnmpd(text)
	print("NMPD: "..text)
	logfile = io.open("serverlog.txt","a*")
	if logfile ~= nil then
		logfile:write("NMPD: "..text.."\n")
		logfile:close()
	end
end

if iTunes == nil then
	printnmpd("Could not connect to iTunes COM server! Shutting down!")
	os.exit(1)
end

local function exportstring( s )
	s = string.format( "%q",s )
	s = string.gsub( s,"\\\n","\\n" )
	s = string.gsub( s,"\r","\\r" )
	s = string.gsub( s,string.char(26),"\"..string.char(26)..\"" )
	return s
end

function table.explode(div,str)
	if (div=='') then return false end
		local pos,arr = 0,{}
		-- for each divider found
		for st,sp in function() return string.find(str,div,pos,true) end do
		table.insert(arr,string.sub(str,pos,st-1)) -- Attach chars left of current divider
		pos = sp + 1 -- Jump past current divider
	end
	table.insert(arr,string.sub(str,pos)) -- Attach chars right of last divider
	return arr
end

function table.save(  tbl,filename )
   local charS,charE = "   ","\n"
   local file,err
   if not filename then
      file =  { write = function( self,newstr ) self.str = self.str..newstr end, str = "" }
      charS,charE = "",""
   elseif filename == true or filename == 1 then
      charS,charE,file = "","",io.tmpfile()
   else
      file,err = io.open( filename, "w" )
      if err then return _,err end
   end
   local tables,lookup = { tbl },{ [tbl] = 1 }
   file:write( "return {"..charE )
   for idx,t in ipairs( tables ) do
      if filename and filename ~= true and filename ~= 1 then
         file:write( "-- Table: {"..idx.."}"..charE )
      end
      file:write( "{"..charE )
      local thandled = {}
      for i,v in ipairs( t ) do
         thandled[i] = true
         if type( v ) ~= "function" and type( v ) ~= "userdata" then
            if type( v ) == "table" then
               if not lookup[v] then
                  table.insert( tables, v )
                  lookup[v] = #tables
               end
               file:write( charS.."{"..lookup[v].."},"..charE )
            else
               local value =  ( type( v ) == "string" and exportstring( v ) ) or tostring( v )
               file:write(  charS..value..","..charE )
            end
         end
      end
      for i,v in pairs( t ) do
         if (not thandled[i]) and type( v ) ~= "function" and type( v ) ~= "userdata" then
            if type( i ) == "table" then
               if not lookup[i] then
                  table.insert( tables,i )
                  lookup[i] = #tables
               end
               file:write( charS.."[{"..lookup[i].."}]=" )
            else
               local index = ( type( i ) == "string" and "["..exportstring( i ).."]" ) or string.format( "[%d]",i )
               file:write( charS..index.."=" )
            end
            if type( v ) == "table" then
               if not lookup[v] then
                  table.insert( tables,v )
                  lookup[v] = #tables
               end
               file:write( "{"..lookup[v].."},"..charE )
            else
               local value =  ( type( v ) == "string" and exportstring( v ) ) or tostring( v )
               file:write( value..","..charE )
            end
         end
      end
      file:write( "},"..charE )
   end
   file:write( "}" )
   if not filename then
      return file.str.."--|"
   elseif filename == true or filename == 1 then
      file:seek ( "set" )
      return file:read( "*a" ).."--|"
   else
      file:close()
      return 1
   end
end

function table.load( sfile )
   if string.sub( sfile,-3,-1 ) == "--|" then
      tables,err = loadstring( sfile )
   else
      tables,err = loadfile( sfile )
   end
   if err then return _,err
   end
   tables = tables()
   for idx = 1,#tables do
      local tolinkv,tolinki = {},{}
      for i,v in pairs( tables[idx] ) do
         if type( v ) == "table" and tables[v[1]] then
            table.insert( tolinkv,{ i,tables[v[1]] } )
         end
         if type( i ) == "table" and tables[i[1]] then
            table.insert( tolinki,{ i,tables[i[1]] } )
         end
      end
      for _,v in ipairs( tolinkv ) do
         tables[idx][v[1]] = v[2]
      end
      for _,v in ipairs( tolinki ) do
         tables[idx][v[2]],tables[idx][v[1]] =  tables[idx][v[1]],nil
      end
   end
   return tables[1]
end

function PlaySong(connection, title, artist, album)
	if title ~= nil and artist ~= nil and album ~= nil then
		local iTrackFile = 1
		local iLibrary = iTunes.LibraryPlaylist
		local iArtistTracks = iLibrary:Search(artist,2)
		if iArtistTracks ~= nil then
			local numtracks = iArtistTracks.Count
			while numtracks ~= 0 do
				local ctrack = iArtistTracks:Item(numtracks)
				if ctrack.Kind == iTrackFile then
					local ctitle = ctrack.Name
					local cartist = ctrack.Artist
					local calbum = ctrack.Album
					local path = ctrack.Location
					if title == ctitle and artist == cartist and album == calbum then
						ctrack:Play()
					end
				end
				numtracks = numtracks - 1
			end
		end
	end
	printnmpd("[OUT] DEBUG NETWORK DATA \"ACKPLAY\"")
	connection:send("ACKPLAY\n")
end

function PlaySearch(connection, searchstring)
	local iTrackFile = 1
	local iLibrary = iTunes.LibraryPlaylist
	local iArtistTracks = iLibrary:Search(searchstring,0)
	if iArtistTracks ~= nil then
		local ctrack = iArtistTracks:Item(iArtistTracks.Count)
		ctrack:Play()
		local title = ctrack.Name
		local artist = ctrack.Artist
		printnmpd("[OUT] DEBUG NETWORK DATA \"ACKPLAYSEARCH ".. artist .."@".. title .."\"")
		connection:send("ACKPLAYSEARCH ".. artist .."@".. title .."\n")
	else
		printnmpd("[OUT] DEBUG NETWORK DATA \"NOSEARCHMATCH\"")
		connection:send("NOSEARCHMATCH\n")
	end
end

function StopSong(connection)
	iTunes:Stop()
	printnmpd("[OUT] DEBUG NETWORK DATA \"ACKSTOP\"")
	connection:send("ACKSTOP\n")
end

function PauseSong(connection)
	iTunes:Pause()
	printnmpd("[OUT] DEBUG NETWORK DATA \"ACKPAUSE\"")
	connection:send("ACKPAUSE\n")
end

function TogglePauseSong(connection)
	iTunes:PlayPause()
	printnmpd("[OUT] DEBUG NETWORK DATA \"ACKTOGGLEPAUSE\"")
	connection:send("ACKTOGGLEPAUSE\n")
end

function GetVolume(connection)
	local volume = iTunes.SoundVolume
	printnmpd("[OUT] DEBUG NETWORK DATA \"CURRENTVOLUME ".. volume .."\"")
	connection:send("CURRENTVOLUME "..volume.."\n")
end

function SetVolume(connection,volume)
	iTunes.SoundVolume = volume
	printnmpd("[OUT] DEBUG NETWORK DATA \"ACKSETVOLUME\"")
	connection:send("ACKSETVOLUME\n")
end

function CurrentTrack(connection)
	local iTrack = iTunes.CurrentTrack
	if iTrack ~= nil then
		printnmpd("[OUT] DEBUG NETWORK DATA \"CURRENTTRACK "..iTrack.Artist.."@"..iTrack.Name.."\"")
		connection:send("CURRENTTRACK "..iTrack.Artist.."@"..iTrack.Name.."\n")
	else
		printnmpd("[OUT] DEBUG NETWORK DATA \"NOTRACKPLAYING\"")
		connection:send("NOTRACKPLAYING\n")
	end
end

function KillServer(passwd)
	if passwd == KILLPASSWORD then
		os.exit()
	else
		printnmpd("Incorrect password to KILLSERVER!")
	end
end

function Version(connection)
	printnmpd("[OUT] DEBUG NETWORK DATA \"VERSION "..VERSION.."\"")
	connection:send("VERSION "..VERSION.."\n")
end

function iTunes.AddEventHandler (EventHandler)
  iTunes.EventHandlers = iTunes.EventHandlers or {}
  if type (EventHandler) == "function" then
    tinsert (iTunes.EventHandlers,EventHandler)
  end
end

function iTunes.CallEventHandlers (event)
  local x
  for x = 1, getn ((iTunes.EventHandlers or {})) do
    iTunes.EventHandlers [x] (event)
  end
end

function iTunesEvents:OnPlayerPlayEvent(iTrack)
	iTunes.CallEventHandlers(1)
	printnmpd("Track changed to "..iTrack.Name.." - "..iTrack.Artist)
	AddEvent("CHANGEDTRACK "..iTrack.Name.." - "..iTrack.Artist)
end
iTunesEventHandler = luacom.ImplInterface(iTunesEvents, "iTunes.Application", "_IiTunesEvents")
luacom.addConnection(iTunes, iTunesEventHandler)
iTunes.Connection = luacom.Connect(iTunes,iTunesEvents)

function GenerateLibrary()
	LIBRARY = {}
	songcount = 0
	local iTrackFile = 1
	local iLibrary = iTunes.LibraryPlaylist
	local tracks = iLibrary.Tracks
	local numtracks = tracks.Count
	while numtracks ~= 0 do
		local ctrack = tracks:Item(numtracks)
		if ctrack.Kind == iTrackFile then
			local title = ctrack.Name
			local artist = ctrack.Artist
			local album = ctrack.Album
			local path = ctrack.Location
			if title ~= nil and artist ~= nil and album ~= nil and path ~= nil then
				if LIBRARY[artist] == nil then LIBRARY[artist] = {} end
				if LIBRARY[artist][album] == nil then LIBRARY[artist][album] = {} end
				if LIBRARY[artist][album][title] == nil then LIBRARY[artist][album][title] = {} end
				LIBRARY[artist][album][title].path = path
				songcount = songcount + 1
			end
		end
		numtracks = numtracks - 1
	end
	printnmpd("New library generated. Songcount: "..songcount)
	table.save(LIBRARY,"library.nmpd")
	libraryfile = io.open("library.nmpd")
	LIBRARYTEXT = string.gsub(string.gsub(libraryfile:read("*a"),"\n","@@"),"\t","$$")
	libraryfile:close()
end

function AddEvent(event)
     for k, v in pairs(trackevents) do
          v[#v+1] = event
     end
end

function SendLibrary(connection)
	connection:send("LIBRARY "..LIBRARYTEXT.."\n")
end

function LoadLibrary()
	LIBRARY = table.load("library.nmpd")
	if LIBRARY == nil then
		printnmpd("No library file found! Generating new library!")
		GenerateLibrary()
	end
end

function ClientHandler(nmpd)
	nmpd = copas.wrap(nmpd)
	connections = connections + 1
	trackevents[nmpd] = {}
	printnmpd("Client connected, total "..connections)
	printnmpd("[OUT] DEBUG NETWORK DATA \"NMPD READY\"")
	nmpd:send("NMPD READY\n")
	local disconnect = false
	while disconnect ~= true do
		for k,v in pairs(trackevents[nmpd]) do
			printnmpd("[OUT] DEBUG NETWORK DATA "..v)
			nmpd:send(v.."\n")
		end
		trackevents[nmpd] = {}
		local message,err = nmpd:receive("*l")
		if err == nil then
			printnmpd("[IN] DEBUG NETWORK DATA \"".. message .."\"")
			if string.sub(message,1,3) == "BYE" then
					connections = connections - 1
					disconnect = true
					printnmpd("Client disconnected, total "..connections)
			elseif string.sub(message,1,4) == "PLAY" and string.sub(message,1,10) ~= "PLAYSEARCH" then
				argstable = table.explode("@@@",string.sub(message, 6))
				local title = argstable[1]
				local artist = argstable[2]
				local album = argstable[3]
				PlaySong(nmpd,title,artist,album)
			elseif string.sub(message,1,10) == "PLAYSEARCH" then
				local searchstring = string.sub(message, 12)
				PlaySearch(nmpd,searchstring)
			elseif message == "STOP" then
				StopSong(nmpd)
			elseif message == "PAUSE" then
				PauseSong(nmpd)
			elseif message == "TOGGLEPAUSE" then
				TogglePauseSong(nmpd)
			elseif message == "GETVOLUME" then
				GetVolume(nmpd)
			elseif string.sub(message,1,9) == "SETVOLUME" then
				local volume = tonumber(string.sub(message,11))
				SetVolume(nmpd,volume)
			elseif message == "CURRENTTRACK" then
				CurrentTrack(nmpd)
			elseif string.sub(message,1,10) == "KILLSERVER" then
				printnmpd("Killing server on request from client!")
				KillServer(string.sub(message,12))
			elseif message == "REGENLIBRARY" then
				GenerateLibrary()
			elseif message == "SENDLIBRARY" then
				SendLibrary(nmpd)
			elseif message == "VERSION" then
				Version(nmpd)
			else
				printnmpd("Unrecognised command from client")
			end
		end
	end
	trackevents[nmpd] = nil
end

LoadLibrary()
copas.addserver(socket.bind("*", PORT), ClientHandler)
printnmpd("NMPD started!")
copas.loop()
